package cn.zyq.charging.device.startup;

import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.util.CollectionUtils;
import cn.zyq.charging.device.pojo.po.StationPO;
import org.springframework.stereotype.Component;
import org.springframework.boot.ApplicationRunner;
import cn.zyq.charging.common.pojo.utils.JsonUtils;
import org.springframework.boot.ApplicationArguments;
import cn.zyq.charging.device.constant.DeviceCacheKey;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.BoundGeoOperations;
import cn.zyq.charging.device.dao.repository.StationRepository;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 缓存预热: 场站信息
 * 场站的基本信息(Hash格式)缓存到Redis中
 * 场站的位置信息(GEO格式)缓存到Redis中
 */
@Slf4j
@Component
public class StationDataInit  implements ApplicationRunner {
    @PostConstruct
    public void init() throws Exception{
        log.info("@PostConstruct初始化充电站数据StationData");
    }

    @Autowired
    private StationRepository stationRepository;

    @Autowired
    private RedisTemplate<String , Serializable> redisTemplate;

    public void run(ApplicationArguments args) throws Exception {
        log.debug("ApplicationRunner---run---StationDataInit...");
        log.debug("缓存预热,在项目启动的时从数据库查询场站位置信息(经纬度),放到Redis里");
        //在预热的时候要考虑数据库表的行数/数据量,如果数据量过大,如何定义过大,数据占用的内存空间,查询的性能,考虑分页查询
        //1 如果数量过大,内存放不下,内存溢出 OutMemoryError  https://blog.csdn.net/TreeShu321/article/details/135255821
        //mysql 分页怎么分 ?  limit 两个参数 1 从哪开始 2 每页多少条数据  一个参数的时候 表示从0 开始 参数表示查多少条
        //2 ApplicationRunner#run 每次启动的时候,都会走 run 这个方法,都会从数据库查数据,放数据到Redis
        //可以先去Redis查询 看有没有数据,
        // (通过标识为 设置一个key 如果这个key value 1或者true 表示预热过了,
        // key 不存在或者 value 等于 0 不存在 表示没有预热)检查数据是否存在
        // 预热过,如果预热过,就不预热了,如果没有预热,就从数据库查询,然后放到redis 进行预热

        //1.从mysql里获取所有的充电站信息(id, 充电站名称， 设备数量，经纬度)
        List<StationPO> stationPOS=stationRepository.getAllStations();
        log.debug("数据库查询场站位置信息成功:{}",stationPOS);
        //从redis中获取geo信息(位置信息)
        //boundGeoOps 需要指定key Geo 表示能操作Redis GEO类型的数据
        //redisTemplate.opsForGeo();  操作GEO数据 //不指定key，使用的时候指定key
        //redisTemplate.boundGeoOps(指定一个key) //需要指定key , key相当于数据库中的一个表。
        //这里不要指定BoundGeoOperations的k-v泛型 ， 如果指定了泛型<String, Serializable>
        //则stations.add(pointMap);会报错，因为add(Map<M,Point>)要求给Redis中添加一个Map集合此集合要实现序列化接口
        //而Map接口没有实现序列化接口，即使将Map写成HashMap也不行(因为缺少实现Iterable接口)。 所以不要指定泛型(指的是不显示添加的元素类型)。
        BoundGeoOperations stations=redisTemplate.boundGeoOps(DeviceCacheKey.STATION_LOCATION_KEY);//STATION_LOCATION_KEY相当于Redis中的一个表。
        //opsForValue 不需要指定key Value 表示能操作Redis String类型的数据
        ValueOperations<String, Serializable> strValueOperations=redisTemplate.opsForValue();
        //opsForHash 不需要指定key Hash 表示能操作Redis Hash类型的数据
        HashOperations<String, Object ,Object>  hashOperations=redisTemplate.opsForHash();
        //GeoOperations<String, Serializable> geoOperations = redisTemplate.opsForGeo();


        //2.循环遍历从数据库查出的场站信息,放到HashMap pointMap中， key场站id ，  value场站经纬度对应的是RedisGEO中的Point
        //注意：循环完毕后,直接保存map,类似批处理. 批量保存,一起保存,不是遍历的保存,从而提高性能
        //放到map key 场站id value Point
        Map<String,Point> pointMap = new HashMap<String,Point>();
        if(!CollectionUtils.isEmpty(stationPOS)){
            //2.1.循环场站列表获取经度维度封装为Point， 将场站数据的id作为key将 id-point 放到pointMap中(再将pointMap存入到Redis)
            for(StationPO  stationPO:stationPOS){
                BigDecimal stationLng = stationPO.getStationLng();//获取经度
                BigDecimal stationLat = stationPO.getStationLat();//获取维度
                //将经度信息转化为String类型：stationLng.toEngineeringString()
                //toString(): 可能使用科学计数法（指数形式）表示非常大或非常小的数值。不推荐用于需要精确控制输出格式的场景
                //toPlainString() : 返回不带任何指数部分的字符串表示形式。
                //特点：不会使用科学计数法，始终以普通数字格式显示。推荐用于展示给用户或写入文件等需要直观可读的场景
                //toEngineeringString(): 使用工程计数法（exponent 必须是 3 的倍数）返回字符串。
                //特点：指数部分始终为 3 的倍数（如 E3, E6, E-9 等），便于阅读和理解。
                //常用于工程、科学等领域，强调单位前缀（如 k、M、G、μ 等）。
                double doubleLng = Double.parseDouble(stationLng.toEngineeringString());
                double doubleLat = Double.parseDouble(stationLat.toEngineeringString());
                Point point = new Point(doubleLng, doubleLat);//通过经度和维度构建 RedisGEO point
                //放到map 做批量保存到redis 准备
                log.debug("通过RedisGEO 保存场站位置信息:key:{},value:{}",stationPO.getId(),  point);
                //String.valueOf(stationPO.getId())：方法是将int类型的参数转换为String类型：
                // 并且里边判断数据如果有问题会返回字符串"null"，避免空指针
                pointMap.put( String.valueOf(stationPO.getId()),   point);//2.1.将id-point 放到pointMap中
                //GEO 存的是场站的位置信息 经纬度 和 id
                // 把场站的详情(地址 充电站名称, 设备数量 ...) 也需要放入到redis , 用什么Redis数据类型? 数据是如何用的,
                // 如何存?:  key 场站id ,value 场站详情 {name:"高级充电站",桩数量:100,枪数量:200}
                // 如何取?:  通过站id 取 场站详情  {name:"高级充电站",桩数量:100,枪数量:200}
                // 如何修改?:  修改桩数量或者枪数量  取处理,改完,放进去 可以

                //String key 场站id ,value 场站详情 {name:"高级充电站",桩数量:100,枪数量:200}
                //Hash  Map<String,Map<String,value>>
                // key 场站id
                //  Map<String,value>    key name value  高级充电站  key 桩数量 value 100  key   枪数量 value 200
                //外面的key 场站id, 里面的key 桩数量  修改值


                //Redis 基本类型 String List Hash Set Zest(SortedSet)
                //注意：基本类型 表示的是什么呢? 表示的是 Redis里的value , K-V结构 基本类型表示的是V  Key是String
                //String value是String                            Map<String,String>   可以 key 场站id ,value 场站详情
                //List  value是List Map套List 可以放重复的, 下标    Map<String,List<>>
                //Hash  value是Map  Map套Map  可以放重复的         Map<String,Map<String,value>>
                //Set   value是Set  Map套Set  不能重复的           Map<String,Set<>>
                //ZSet   value是set  Map套Set  不能重复的,还能排序   Map<String,TreeSet<>>

                //Set Map<String,Set<>> key 站id ,value 详情


                //2.2.通过Redis的String类型保存充电站的详情对象stationPO(包含所有信息)  //充电站详情String信息缓存key  station:detail:string:
                String stationInfoStringKey = DeviceCacheKey.STATION_DETAIL_STRING_KEY + stationPO.getId();
                String json= JsonUtils.toJson(stationPO);//调用JsonUtils工具类的toJson()方法将stationPO对象转化为json字符串
                strValueOperations.set(stationInfoStringKey, json);//将String类型的value信息json保存到redis中
                log.debug("通过Redis基本类型String 保存场站详情:key:{},value:{}",stationInfoStringKey,json);

                //2.3.通过Redis Hash 类型保存充电站的详情(没有经度维度位置信息)
                // Map<String,Map<String,value>> 外面map的key是 stationInfoHashKey ，value 是 smallMap
                // smallMap key-value 是场站具体的信息,把每个字段都作为smallMap的key,字段的值作为 smallMap的value
                String stationInfoHashKey = DeviceCacheKey.STATION_DETAIL_HASH_KEY + stationPO.getId();
                Map<String, Object> smallMap=new HashMap<>();
                smallMap.put("name",stationPO.getStationName());
                smallMap.put("deviceNumber",stationPO.getDeviceNumber());//smallMap存入设备数量
                smallMap.put("operatorId",stationPO.getOperatorId());//smallMap存入运营商id
                hashOperations.putAll(stationInfoHashKey,smallMap);
                log.debug("通过Redis基本类型Hash 保存场站详情:key:{},value:{}",stationInfoHashKey,smallMap);

                /*Map<String, Set<StationPO>> map = new  HashMap<String,Set<StationPO>>();
                HashSet<StationPO> set = new HashSet<>();
                set.add(stationPO);
                map.put(String.valueOf(stationPO.getId()),set);

                Set<StationPO> stationPOS1 = map.get(String.valueOf(stationPO.getId()));*/
            }
            //5.将存储了所有的充电站位置信息Map集合pointMap到RedisGEO
            //stations.add(GeoLocation);
            //stations.add(Point p,Object Member);
            //stations.radius()//radius:半径
            stations.add(pointMap);
            //使用stations给Redis添加一个Map比stations.add(Point p,Object Member)添加一个点数据
            //效率更高，类似于批量添加。
        }
    }
}
